{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 59,
   "id": "38f3c3eb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "正在爬取好评...\n",
      "正在爬取好评...\n",
      "正在爬取中评...\n",
      "正在爬取中评...\n",
      "正在爬取差评...\n",
      "总获取评价数：1774\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>内容</th>\n",
       "      <th>评分</th>\n",
       "      <th>时间</th>\n",
       "      <th>评价类型</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>此用户未填写评价内容</td>\n",
       "      <td>5</td>\n",
       "      <td>2025-04-19 12:10:13</td>\n",
       "      <td>好评</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>此用户未及时填写评价内容，系统默认评价！</td>\n",
       "      <td>5</td>\n",
       "      <td>2025-04-17 09:36:43</td>\n",
       "      <td>好评</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>此用户未填写评价内容</td>\n",
       "      <td>5</td>\n",
       "      <td>2025-04-16 23:57:32</td>\n",
       "      <td>好评</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>使用了一天说一下我的感受，他的这个144hz高刷屏体验是不错的，就是不知道为什么游戏里不能开...</td>\n",
       "      <td>4</td>\n",
       "      <td>2025-04-16 18:03:08</td>\n",
       "      <td>好评</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>此用户未及时填写评价内容，系统默认评价！</td>\n",
       "      <td>5</td>\n",
       "      <td>2025-04-13 18:27:18</td>\n",
       "      <td>好评</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                                  内容  评分                   时间  \\\n",
       "0                                         此用户未填写评价内容   5  2025-04-19 12:10:13   \n",
       "1                               此用户未及时填写评价内容，系统默认评价！   5  2025-04-17 09:36:43   \n",
       "2                                         此用户未填写评价内容   5  2025-04-16 23:57:32   \n",
       "3  使用了一天说一下我的感受，他的这个144hz高刷屏体验是不错的，就是不知道为什么游戏里不能开...   4  2025-04-16 18:03:08   \n",
       "4                               此用户未及时填写评价内容，系统默认评价！   5  2025-04-13 18:27:18   \n",
       "\n",
       "  评价类型  \n",
       "0   好评  \n",
       "1   好评  \n",
       "2   好评  \n",
       "3   好评  \n",
       "4   好评  "
      ]
     },
     "execution_count": 59,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Cell 1 - 全量评价爬取\n",
    "import requests\n",
    "import json\n",
    "import time\n",
    "import pandas as pd\n",
    "# from fake_useragent import UserAgent\n",
    "import random\n",
    "\n",
    "# 代理池配置\n",
    "PROXY_POOL = [\n",
    "    {'http': 'http://123.123.123:8080'},\n",
    "    {'http': 'http://124.124.124:8080'}\n",
    "]\n",
    "\n",
    "def dynamic_fetch(product_id, page, score=0):\n",
    "    \"\"\"动态请求函数\"\"\"\n",
    "#     ua = UserAgent()\n",
    "    url = f'https://club.jd.com/comment/productPageComments.action?callback=fetchJSON_comment98&productId={product_id}&pageSize=10&score={score}&sortType=5&page={page-1}'\n",
    "    \n",
    "    for _ in range(3):  # 重试机制\n",
    "        try:\n",
    "            resp = requests.get(\n",
    "                url,\n",
    "                headers={'User-Agent': \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0\"},\n",
    "                proxies=random.choice(PROXY_POOL),\n",
    "                timeout=random.uniform(1,3)\n",
    "            )\n",
    "            if resp.status_code == 200:\n",
    "                json_str = resp.text[resp.text.find('(')+1:resp.text.rfind(')')]\n",
    "                data = json.loads(json_str)\n",
    "                return data.get('comments', [])\n",
    "        except Exception as e:\n",
    "            print(f\"请求失败: {str(e)}\")\n",
    "            time.sleep(2)\n",
    "    return []\n",
    "\n",
    "def full_scrape(product_id, max_pages=100):\n",
    "    \"\"\"全量评价爬取\"\"\"\n",
    "    all_comments = []\n",
    "    \n",
    "    # 定义评分类型映射\n",
    "    score_types = {5: '好评',4: '好评', 3: '中评',2: '差评', 1: '差评'}\n",
    "    for score, label in score_types.items():\n",
    "        page = 1\n",
    "        count = 0\n",
    "        print(f\"正在爬取{label}...\")\n",
    "        while page <= max_pages:\n",
    "            count = count +1\n",
    "            comments = dynamic_fetch(product_id, page, score)\n",
    "            if not comments:\n",
    "                break\n",
    "                \n",
    "            # 保持原始数据结构\n",
    "            all_comments.extend([{\n",
    "                '内容': c.get('content', ''),\n",
    "                '评分': c.get('score', 0),\n",
    "                '时间': c.get('creationTime', ''),\n",
    "                '评价类型': label  \n",
    "            } for c in comments])\n",
    "            \n",
    "            page += 1\n",
    "            time.sleep(random.uniform(0.5,1.5))\n",
    "    \n",
    "    df = pd.DataFrame(all_comments)\n",
    "    # 保持原始列顺序\n",
    "    return df[['内容', '评分', '时间', '评价类型']]\n",
    "\n",
    "# 执行爬取\n",
    "product_id = \"100129509823\"\n",
    "df = full_scrape(product_id)\n",
    "print(f\"总获取评价数：{len(df)}\")\n",
    "df.head()\n",
    "# df.to_csv(\"jd_comments.csv\",index=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "id": "cd412122",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Exception managing chrome: error sending request for url (https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "正在爬取 中评 (score=2)...\n",
      "正在爬取 差评 (score=1)...\n",
      "总获取评价数：528\n"
     ]
    }
   ],
   "source": [
    "from selenium import webdriver\n",
    "from selenium.webdriver.chrome.options import Options\n",
    "import json\n",
    "import time\n",
    "import random\n",
    "import pandas as pd\n",
    "import os\n",
    "\n",
    "class JDCommentsSelenium:\n",
    "    def __init__(self, product_id, max_pages=100, headless=True):\n",
    "        self.product_id = product_id\n",
    "        self.max_pages = max_pages\n",
    "        self.score_types = {2: '中评', 1: '差评'}\n",
    "\n",
    "        # 配置 Chrome WebDriver\n",
    "        options = Options()\n",
    "        if headless:\n",
    "            options.add_argument('--headless')\n",
    "        options.add_argument('--disable-gpu')\n",
    "        options.add_argument('--no-sandbox')\n",
    "        options.add_argument(\n",
    "            'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' \n",
    "            'AppleWebKit/537.36 (KHTML, like Gecko) ' \n",
    "            'Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0'\n",
    "        )\n",
    "        \n",
    "        options.add_argument('--proxy-server=http://123.123.123:8080')\n",
    "\n",
    "        self.driver = webdriver.Chrome(options=options)\n",
    "\n",
    "    def fetch_comments(self):\n",
    "        all_comments = []\n",
    "        base_url = (\n",
    "            'https://club.jd.com/comment/productPageComments.action'\n",
    "            '?callback=fetchJSON_comment98'\n",
    "            '&productId={product_id}'\n",
    "            '&pageSize=10'\n",
    "            '&score={score}'\n",
    "            '&sortType=5'\n",
    "            '&page={page}'\n",
    "        )\n",
    "\n",
    "        for score, label in self.score_types.items():\n",
    "            print(f\"正在爬取 {label} (score={score})...\")\n",
    "            for page in range(self.max_pages):\n",
    "                url = base_url.format(\n",
    "                    product_id=self.product_id,\n",
    "                    score=score,\n",
    "                    page=page\n",
    "                )\n",
    "                try:\n",
    "                    self.driver.get(url)\n",
    "                    # 给页面一些时间加载\n",
    "                    time.sleep(random.uniform(0.5, 1.5))\n",
    "                    text = self.driver.page_source\n",
    "                    # 提取 JSONP 内容\n",
    "                    start = text.find('fetchJSON_comment98(')\n",
    "                    if start == -1:\n",
    "                        break\n",
    "                    start = text.find('(', start) + 1\n",
    "                    end = text.rfind(')')\n",
    "                    json_str = text[start:end]\n",
    "                    data = json.loads(json_str)\n",
    "                    comments = data.get('comments', [])\n",
    "                    if not comments:\n",
    "                        break\n",
    "                    for c in comments:\n",
    "                        all_comments.append({\n",
    "                            '内容': c.get('content', ''),\n",
    "                            '评分': c.get('score', 0),\n",
    "                            '时间': c.get('creationTime', ''),\n",
    "                            '评价类型': label\n",
    "                        })\n",
    "                except Exception as e:\n",
    "                    print(f\"Error fetching page {page+1} score {score}: {e}\")\n",
    "                    time.sleep(2)\n",
    "                    continue\n",
    "\n",
    "        return pd.DataFrame(all_comments, columns=['内容', '评分', '时间', '评价类型'])\n",
    "\n",
    "    def close(self):\n",
    "        self.driver.quit()\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    product_id = '100135187065'\n",
    "    output_file = 'jd_comments.csv'\n",
    "\n",
    "    scraper = JDCommentsSelenium(product_id=product_id, max_pages=100, headless=True)\n",
    "    df = scraper.fetch_comments()\n",
    "    print(f\"总获取评价数：{len(df)}\")\n",
    "\n",
    "    # 如果文件已存在，则追加，否则写入新文件并包含表头\n",
    "    if os.path.exists(output_file):\n",
    "        df.to_csv(output_file, mode='a', header=False, index=False)\n",
    "    else:\n",
    "        df.to_csv(output_file, mode='w', header=True, index=False)\n",
    "\n",
    "    scraper.close()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "262fad2b",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "id": "a7d9a522",
   "metadata": {},
   "outputs": [],
   "source": [
    "data = pd.read_csv('jd_comments.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 65,
   "id": "8046bcb6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>内容</th>\n",
       "      <th>评分</th>\n",
       "      <th>时间</th>\n",
       "      <th>评价类型</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>真的很舒服打游戏没压力，跑分280万，目前打游戏没怎么掉过帧，原神插帧到120，也不会怎么掉...</td>\n",
       "      <td>5</td>\n",
       "      <td>2024-11-26 16:24:59</td>\n",
       "      <td>好评</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>iQOO 13作为一款旗舰级手机，其强悍的配置和丰富的功能令人印象深刻。首先，其搭载的蓝晶x...</td>\n",
       "      <td>5</td>\n",
       "      <td>2024-11-19 07:36:13</td>\n",
       "      <td>好评</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>手机颜值很高，传奇色耐看，自研芯片性能很强，电池续航能力也不错，打游戏首选准没错！推荐购买！</td>\n",
       "      <td>5</td>\n",
       "      <td>2024-11-28 14:37:41</td>\n",
       "      <td>好评</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>续航很好运行畅快拍照清晰iQ0013使用有一\\n天了，不愧为今年电竞机皇，非常丝滑，原\\n神...</td>\n",
       "      <td>5</td>\n",
       "      <td>2024-11-26 21:42:58</td>\n",
       "      <td>好评</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>新手机收到了，颜色很酷，游戏性能也很好。给我一个好评吧，它真的很棒，不愧是最酷的手机。优酷官...</td>\n",
       "      <td>5</td>\n",
       "      <td>2024-11-29 14:48:24</td>\n",
       "      <td>好评</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6211</th>\n",
       "      <td>我不知道是为什么，手机拿来就是坏的，充电的时候突然充不进去电，等一下又充进去，反复如此。后来...</td>\n",
       "      <td>1</td>\n",
       "      <td>2025-04-18 01:14:28</td>\n",
       "      <td>差评</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6212</th>\n",
       "      <td>打开个应用需要10秒，手机上面基本上就一个~和~，看了一下系统信息感觉怪怪&amp;amp;hell...</td>\n",
       "      <td>1</td>\n",
       "      <td>2025-03-14 15:50:29</td>\n",
       "      <td>差评</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6213</th>\n",
       "      <td>差评差评，绝对差评，差的是负分，负一百分，首先，手机质量差评，买回来还没几天，手机放那里充电...</td>\n",
       "      <td>1</td>\n",
       "      <td>2025-02-18 17:50:10</td>\n",
       "      <td>差评</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6214</th>\n",
       "      <td>手机本身太差了，虽然不贵但作为一台手机最基本的运行顺畅都做不到，只能说广告打得太好，产品做得...</td>\n",
       "      <td>1</td>\n",
       "      <td>2025-04-10 21:06:36</td>\n",
       "      <td>差评</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6215</th>\n",
       "      <td>手机卡的要死，新手机点开~要五六秒反应，调节声音那个按键也不灵敏，下调或者提高音量按一下需要...</td>\n",
       "      <td>1</td>\n",
       "      <td>2025-03-16 22:43:46</td>\n",
       "      <td>差评</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>6216 rows × 4 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                                                     内容  评分  \\\n",
       "0     真的很舒服打游戏没压力，跑分280万，目前打游戏没怎么掉过帧，原神插帧到120，也不会怎么掉...   5   \n",
       "1     iQOO 13作为一款旗舰级手机，其强悍的配置和丰富的功能令人印象深刻。首先，其搭载的蓝晶x...   5   \n",
       "2        手机颜值很高，传奇色耐看，自研芯片性能很强，电池续航能力也不错，打游戏首选准没错！推荐购买！   5   \n",
       "3     续航很好运行畅快拍照清晰iQ0013使用有一\\n天了，不愧为今年电竞机皇，非常丝滑，原\\n神...   5   \n",
       "4     新手机收到了，颜色很酷，游戏性能也很好。给我一个好评吧，它真的很棒，不愧是最酷的手机。优酷官...   5   \n",
       "...                                                 ...  ..   \n",
       "6211  我不知道是为什么，手机拿来就是坏的，充电的时候突然充不进去电，等一下又充进去，反复如此。后来...   1   \n",
       "6212  打开个应用需要10秒，手机上面基本上就一个~和~，看了一下系统信息感觉怪怪&amp;hell...   1   \n",
       "6213  差评差评，绝对差评，差的是负分，负一百分，首先，手机质量差评，买回来还没几天，手机放那里充电...   1   \n",
       "6214  手机本身太差了，虽然不贵但作为一台手机最基本的运行顺畅都做不到，只能说广告打得太好，产品做得...   1   \n",
       "6215  手机卡的要死，新手机点开~要五六秒反应，调节声音那个按键也不灵敏，下调或者提高音量按一下需要...   1   \n",
       "\n",
       "                       时间 评价类型  \n",
       "0     2024-11-26 16:24:59   好评  \n",
       "1     2024-11-19 07:36:13   好评  \n",
       "2     2024-11-28 14:37:41   好评  \n",
       "3     2024-11-26 21:42:58   好评  \n",
       "4     2024-11-29 14:48:24   好评  \n",
       "...                   ...  ...  \n",
       "6211  2025-04-18 01:14:28   差评  \n",
       "6212  2025-03-14 15:50:29   差评  \n",
       "6213  2025-02-18 17:50:10   差评  \n",
       "6214  2025-04-10 21:06:36   差评  \n",
       "6215  2025-03-16 22:43:46   差评  \n",
       "\n",
       "[6216 rows x 4 columns]"
      ]
     },
     "execution_count": 65,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 61,
   "id": "96ee9c0d",
   "metadata": {},
   "outputs": [],
   "source": [
    "data = data.drop(data[data[\"内容\"]==\"此用户未填写评价内容\"].index)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 63,
   "id": "f13ee2fd",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "评价类型\n",
      "中评    1000\n",
      "好评    1000\n",
      "差评    1000\n",
      "Name: count, dtype: int64\n"
     ]
    }
   ],
   "source": [
    "good_samples = data[data['评价类型'] == '好评'].sample(n=1000, random_state=42)\n",
    "mid_samples  = data[data['评价类型'] == '中评'].sample(n=1000, random_state=42)\n",
    "bad_samples  = data[data['评价类型'] == '差评'].sample(n=1000, random_state=42)\n",
    "\n",
    "# （3）合并并打乱顺序\n",
    "sampled_data = pd.concat([good_samples, mid_samples, bad_samples]) \\\n",
    "               .sample(frac=1, random_state=42) \\\n",
    "               .reset_index(drop=True)\n",
    "\n",
    "# （4）看看结果\n",
    "print(sampled_data['评价类型'].value_counts())\n",
    "\n",
    "# （5）如果需要，保存到新 CSV\n",
    "sampled_data.to_csv('jd_comments_sampled.csv', index=False)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
